Skip to content

Gui/site builder#2455

Open
vibegui wants to merge 127 commits intomainfrom
gui/site-builder
Open

Gui/site builder#2455
vibegui wants to merge 127 commits intomainfrom
gui/site-builder

Conversation

@vibegui
Copy link
Contributor

@vibegui vibegui commented Feb 15, 2026

Summary by cubic

Builds a full site editor with pages, sections, loaders, and a robust live preview bridge (click‑to‑select, edit/interact, reconnect). Adds multi‑site switching with unsaved‑changes protection and publishes the canonical Blocks Framework spec. Extends SITE_BINDING with file/history/git tools to enable branches, diffs, and revert, and validates the end‑to‑end editor flow.

  • New Features

    • Plugin: SITE_BINDING (READ/PUT/LIST) with optional branch, history, and git tools; plugin registered with routes for pages/sections/loaders.
    • Pages: CRUD tools + list/editor UIs; Page Composer with DnD reorder, BlockPicker, RJSF PropEditor, undo/redo, page variants; history timeline with diff and one‑click revert.
    • Blocks: ts‑morph scanner (extract/discover/schema), sections list with collapsible categories, block detail with interactive schema tree.
    • Loaders: discovery + JSON Schema, list/detail UIs, prop binding into section props, connected sections mapping.
    • Preview bridge: single postMessage path, Vite auto‑inject plugin, click‑to‑select, edit/interact toggle, disconnect/reconnect, live prop hot‑swap.
    • Multi‑site UX: Site Switcher + palette, unsaved‑changes dialog with save‑and‑switch, root‑dir reset fix, tunnel auto‑detection and preview URL persistence.
    • Blocks Framework: spec added (+ /deco:blocks-framework and starter template copy).
    • Developer tooling: adds .claude GSD agents, workflows, and gsd‑tools CLI for planning, execution, debugging, and verification.
  • Migration

    • Connect a site in Site Editor → choose local folder (validated tsconfig/package.json) → create connection.
    • Run “deco link”; tunnel is auto‑detected and the preview URL is persisted, or enter a local URL. Add decoEditorBridgePlugin or use the explicit client bridge.
    • Use the top bar to add or switch sites; confirm, discard, or save‑and‑switch if prompted for unsaved changes.

Written for commit d1a3f10. Summary will update on new commits.

vibegui and others added 26 commits February 14, 2026 09:18
- Add SITE_BINDING with READ_FILE, PUT_FILE, LIST_FILES tool binders
- Export SITE_BINDING from @decocms/bindings and add ./site entry point
- Create mesh-plugin-site-editor package with server/client entry points
- Add shared.ts with PLUGIN_ID and PLUGIN_DESCRIPTION constants

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- PreviewPanel renders iframe pointing to tunnel URL with sandbox attrs
- useTunnelUrl resolves preview URL from connection metadata
- Empty state shows instructions to run `deco link`
- Component accepts path prop for page-specific previews

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Add ServerPlugin with empty tools array (tools added in plan 01-03)
- Add ClientPlugin with SITE_BINDING, sidebar groups (Pages, Sections, Loaders)
- Create plugin router with 4 routes (/, /pages/$pageId, /sections, /loaders)
- Add stub page components, header, and empty state
- Register server plugin in apps/mesh/src/server-plugins.ts
- Register client plugin in apps/mesh/src/web/plugins.ts

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- CMS_PAGE_LIST: lists pages from .deco/pages/ via LIST_FILES + READ_FILE
- CMS_PAGE_GET: reads single page by ID from .deco/pages/{id}.json
- CMS_PAGE_CREATE: creates page with nanoid ID, writes via PUT_FILE
- CMS_PAGE_UPDATE: reads existing page, merges fields, writes back
- CMS_PAGE_DELETE: tries DELETE_FILE, falls back to tombstone JSON
- All tools use try/finally with proxy.close?.() to prevent leaks

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- pages-list.tsx: table view with create dialog, delete action, navigation
- page-editor.tsx: form for title/path with save, breadcrumb, metadata display
- page-api.ts: client-side CRUD helpers using SITE_BINDING tools directly
- query-keys.ts: structured React Query keys for page caching
- router.ts: updated /pages/$pageId route to point to page-editor component

Deviation: Client calls SITE_BINDING tools (READ_FILE, PUT_FILE, LIST_FILES)
directly via page-api helpers instead of CMS_PAGE_* server tools, because
the plugin toolCaller is connected to the site MCP (not the SELF MCP where
server tools live). Server tools still exist for AI agent access via SELF MCP.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Add ts-morph and ts-json-schema-generator dependencies
- Create BlockDefinition, ComponentInfo, ScanResult types in types.ts
- Implement createProjectFromMCP in extract.ts (in-memory ts-morph from MCP files)
- Implement discoverComponents in discover.ts (default + named export detection)
- Implement generateSchema in schema.ts (JSON Schema generation with $ref inlining)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Add CMS_BLOCK_SCAN tool (ts-morph pipeline: extract -> discover -> schema -> write)
- Add CMS_BLOCK_LIST tool (read .deco/blocks/ summaries)
- Add CMS_BLOCK_GET tool (full block definition with schema)
- Add CMS_BLOCK_REGISTER tool (manual block registration)
- Register all 4 block tools in server/tools/index.ts (now 9 total)
- Add client block-api with listBlocks and getBlock via SITE_BINDING
- Add block query keys to query-keys.ts

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Custom FieldTemplate with type icons, labels, required indicators
- Custom ObjectFieldTemplate with left-border indent for nested objects
- Custom ArrayFieldTemplate with add/remove controls
- TextWidget, NumberWidget, CheckboxWidget, SelectWidget using @deco/ui
- PropEditor wrapper component rendering @rjsf/core Form
- Added @rjsf/core, @rjsf/utils, @rjsf/validator-ajv8, lucide-react deps

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- SectionsList: block browser grouped by category with empty state
- BlockDetail: metadata display, collapsible JSON Schema, PropEditor form
- Router: added /sections/$blockId route for block detail navigation
- Block list fetches via listBlocks from block-api with React Query
- Block detail uses ref-based formData sync pattern (no useEffect)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Add editor-protocol.ts with EditorMessage/SiteMessage discriminated unions
- Add use-editor-messages.ts hook with send/subscribe and source filtering
- Add BlockInstance interface to page-api.ts replacing unknown[] in Page.blocks

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…iew panel

- Add viewport-toggle.tsx with mobile/tablet/desktop at 375/768/1440px
- Rewrite preview-panel.tsx with postMessage protocol via useIframeBridge
- Add use-iframe-bridge.ts using useSyncExternalStore for ready state
- Add page-composer.tsx three-panel layout with section list, preview, prop editor

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…cker

- SectionListSidebar with DnD reordering, selection, and delete
- BlockPicker modal with category grouping and search filter
- Added @dnd-kit/core, @dnd-kit/sortable, @dnd-kit/modifiers, @dnd-kit/utilities deps

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…D reorder

- Replace left panel placeholder with SectionListSidebar + DnD reordering via arrayMove
- Add BlockPicker modal for adding new sections from block library
- Wire onDelete, onReorder, onAddClick handlers with debounced save to git
- Invalidate query cache on successful save
- Rewrite page-editor.tsx to render PageComposer instead of metadata form

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Generic useUndoRedo<T> hook with push/undo/redo/reset/clearFuture
- Atomic state transitions via useReducer (past/present/future)
- 100-entry cap on past stack to prevent unbounded memory growth
- clearFuture action for clearing redo stack after save

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…rtcuts

- Replace local page state with useUndoRedo<BlockInstance[]> hook
- All edit operations (prop change, reorder, add, delete) push snapshots
- Keyboard shortcuts: Cmd+Z undo, Cmd+Shift+Z/Cmd+Y redo
- Undo/redo toolbar buttons with disabled state when stack empty
- Preview iframe updates on undo/redo via deco:page-config postMessage
- Redo stack cleared on save (debounced and manual) to prevent divergence

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Add LoaderInfo, LoaderDefinition, LoaderSummary, LoaderRef types to scanner types
- Add discoverLoaders() function that scans .ts files for default-exported functions
- Extract both input Props type and unwrapped Promise return type
- Skip functions with JSX return types (those are components)
- Support zero-parameter loaders (propsTypeName = null)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Add CMS_LOADER_SCAN tool: discovers loaders, generates input/output schemas, writes to .deco/loaders/
- Add CMS_LOADER_LIST tool: lists loader definitions with summaries
- Add CMS_LOADER_GET tool: reads full loader definition by ID
- Register all three tools in server/tools/index.ts
- Add client loader-api.ts with listLoaders and getLoader helpers via SITE_BINDING
- Add loaders namespace to query-keys.ts with all/detail keys

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Replace loaders-list stub with full categorized list using loader-api
- Create loader-detail with metadata, output schema, and PropEditor for input params
- Add /loaders/$loaderId route to plugin router

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Add LoaderRef type and isLoaderRef guard to page-api
- Create loader-picker modal with category grouping and search filter
- Integrate loader binding into page-composer prop editing panel
- Show existing bindings with remove option, bind-to-prop links for each schema prop

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…example sections

- package.json with React 19, React Router 7, Vite 7, Tailwind CSS 4
- react-router.config.ts with prerender reading .deco/pages/
- Three example sections (hero, features, footer) with typed prop interfaces
- Products loader with typed input/output
- Home route and catch-all route rendering sections from page config
- Root layout with Tailwind CSS import and cn utility
- Add CREATE_BRANCH, LIST_BRANCHES, MERGE_BRANCH, DELETE_BRANCH to SITE_BINDING (optional)
- Create 4 server tools (CMS_BRANCH_LIST/CREATE/MERGE/DELETE)
- Create client branch-api.ts with graceful degradation
- Add DRAFT_BRANCH_PREFIX constant to shared.ts
- Add branches query key group

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
… loaders

- Home page config with Hero, Features, Footer block instances
- Block definitions with JSON Schema matching TypeScript prop interfaces
- Loader definition with input/output schemas
- README with quick start, project structure, and usage guide
- Create BranchSwitcher dropdown with draft creation inline form
- Create PublishBar with merge-to-main and discard actions
- Integrate both into plugin-header with lazy loading
- Use useSyncExternalStore-based branch store for cross-tree state

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…/client APIs

- Add GET_FILE_HISTORY and READ_FILE_AT optional tool binders to SITE_BINDING (9 total)
- Create CMS_FILE_HISTORY and CMS_FILE_READ_AT server tools in page-history.ts
- Create client history-api.ts with getFileHistory, readFileAt, revertPage
- Add history query key group to query-keys.ts
- Revert implemented as read-old-content + PUT_FILE (non-destructive)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…tion

- Create PageHistory component with vertical timeline, relative timestamps, inline revert confirmation
- Create PageDiff component with structured property-level comparison (scalar fields + block changes)
- Add Clock icon button to page composer toolbar toggling history panel in right sidebar
- History panel shows author, message, view-diff, and one-click revert per version
- Diff view uses green/red backgrounds for added/removed blocks and prop changes

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…nnect

- Per-plugin static routes to fix collision between site-editor and object-storage
- Root directory discovery via list_allowed_directories for correct path resolution
- Plugin router strips ID-only layout route prefix from URLs
- Preview panel with inline URL input form to connect dev server
- Preview URL persisted in connection metadata via COLLECTION_CONNECTIONS_UPDATE

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@github-actions
Copy link
Contributor

🧪 Benchmark

Should we run the Virtual MCP strategy benchmark for this PR?

React with 👍 to run the benchmark.

Reaction Action
👍 Run quick benchmark (10 & 128 tools)

Benchmark will run on the next push after you react.

@github-actions
Copy link
Contributor

github-actions bot commented Feb 15, 2026

Release Options

Should a new version be published when this PR is merged?

React with an emoji to vote on the release type:

Reaction Type Next Version
👍 Prerelease 2.108.3-alpha.1
🎉 Patch 2.108.3
❤️ Minor 2.109.0
🚀 Major 3.0.0

Current version: 2.108.2

Deployment

  • Deploy to production (triggers ArgoCD sync after Docker image is published)

Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

34 issues found across 102 files

Note: This PR contains a large number of files. cubic only reviews up to 75 files per PR, so some files may not have been reviewed.

Prompt for AI agents (all issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="packages/mesh-plugin-site-editor/client/components/pages-list.tsx">

<violation number="1" location="packages/mesh-plugin-site-editor/client/components/pages-list.tsx:40">
P2: `toLocaleDateString` only formats the date portion, so the hour/minute options won’t be reflected. Use `toLocaleString` if you want the time included for “Last Updated.”</violation>

<violation number="2" location="packages/mesh-plugin-site-editor/client/components/pages-list.tsx:228">
P2: Avoid nesting a `<Button>` inside the row `<button>`—nested buttons are invalid HTML and can break accessibility/interaction. Use a non-button container for the row (e.g., a `<div role="button">`) or render the action as a non-button element.</violation>
</file>

<file name="packages/mesh-plugin-site-editor/client/components/loaders-list.tsx">

<violation number="1" location="packages/mesh-plugin-site-editor/client/components/loaders-list.tsx:100">
P2: The “Scan Codebase” button is non-functional (only logs to console), which is misleading for users. Until the scan action is wired, disable the button or hook it to the real scan handler.</violation>
</file>

<file name="packages/mesh-plugin-site-editor/client/lib/branch-api.ts">

<violation number="1" location="packages/mesh-plugin-site-editor/client/lib/branch-api.ts:30">
P2: Catching all errors and returning null hides real failures (e.g., network/auth errors) and makes them indistinguishable from “tool not supported.” Consider only returning null for the unsupported-tool error and rethrowing others.</violation>
</file>

<file name="packages/mesh-plugin-site-editor/client/components/rjsf/widgets.tsx">

<violation number="1" location="packages/mesh-plugin-site-editor/client/components/rjsf/widgets.tsx:109">
P2: SelectWidget always passes a string to RJSF. For numeric/boolean enum options this changes the value type (e.g., 1 → "1"), which can break schema validation. Map the selected string back to the original enum option value before calling onChange.</violation>
</file>

<file name="packages/mesh-plugin-site-editor/server/tools/branch-merge.ts">

<violation number="1" location="packages/mesh-plugin-site-editor/server/tools/branch-merge.ts:42">
P2: The schema description says the target defaults to "main", but the handler passes `target` through as `undefined`. This can lead to inconsistent behavior if the MCP tool doesn’t apply its own default. Apply the default in the handler (or in the schema) to match the documented behavior.</violation>
</file>

<file name="packages/mesh-plugin-site-editor/client/components/block-picker.tsx">

<violation number="1" location="packages/mesh-plugin-site-editor/client/components/block-picker.tsx:77">
P2: New blocks are always created with empty props because the picker passes `{}` instead of the block definition defaults. This skips any default props defined in the block schema and can lead to missing required values on insert.</violation>
</file>

<file name="packages/mesh-plugin-site-editor/client/components/publish-bar.tsx">

<violation number="1" location="packages/mesh-plugin-site-editor/client/components/publish-bar.tsx:49">
P2: The success flash is unreachable after publishing because the component returns null as soon as currentBranch becomes "main". This prevents the "Published successfully" banner from ever displaying.</violation>
</file>

<file name="packages/mesh-plugin-site-editor/server/tools/page-delete.ts">

<violation number="1" location="packages/mesh-plugin-site-editor/server/tools/page-delete.ts:22">
P1: Validate/sanitize pageId before building the file path; as written, arbitrary path segments (e.g., "../") can escape `.deco/pages` and delete/overwrite unintended files.</violation>
</file>

<file name="packages/mesh-plugin-site-editor/server/tools/loader-get.ts">

<violation number="1" location="packages/mesh-plugin-site-editor/server/tools/loader-get.ts:18">
P1: Validate `loaderId` to prevent path traversal before using it in a file path.</violation>
</file>

<file name="packages/mesh-plugin-site-editor/server/tools/block-list.ts">

<violation number="1" location="packages/mesh-plugin-site-editor/server/tools/block-list.ts:85">
P2: Ensure `label` is always a string before pushing to `blocks`, otherwise `localeCompare` can throw and the output schema is violated for blocks missing a label.</violation>
</file>

<file name="packages/mesh-plugin-site-editor/server/tools/branch-create.ts">

<violation number="1" location="packages/mesh-plugin-site-editor/server/tools/branch-create.ts:37">
P2: The handler advertises a default source branch of "main" but forwards `from` as-is. If `from` is omitted, the call sends `from: undefined`, so the default may not be applied. Provide an explicit default when building the tool arguments.</violation>
</file>

<file name="packages/mesh-plugin-site-editor/client/lib/use-tunnel-url.ts">

<violation number="1" location="packages/mesh-plugin-site-editor/client/lib/use-tunnel-url.ts:71">
P2: Avoid invalidating all React Query caches here; it triggers unnecessary refetches. Invalidate only the connections query (or the specific query backing `connection` metadata).</violation>
</file>

<file name="apps/mesh/src/web/layouts/plugin-layout.tsx">

<violation number="1" location="apps/mesh/src/web/layouts/plugin-layout.tsx:165">
P2: `PUT_FILE` response adapter unconditionally returns `{ success: true }` without inspecting the actual result. Consider checking `rawResult` for error indicators (e.g., `isError` field) to avoid silently masking write failures.</violation>
</file>

<file name="packages/mesh-plugin-site-editor/client/lib/page-api.ts">

<violation number="1" location="packages/mesh-plugin-site-editor/client/lib/page-api.ts:169">
P2: Prevent updating tombstoned pages; updating a deleted page will write a malformed Page because the tombstone lacks id/path/blocks. Add a deleted check after parsing and fail early (or return) instead of applying updates.</violation>
</file>

<file name="packages/mesh-plugin-site-editor/client/lib/use-editor-messages.ts">

<violation number="1" location="packages/mesh-plugin-site-editor/client/lib/use-editor-messages.ts:30">
P1: Avoid using "*" for postMessage targetOrigin; restrict it to the iframe’s expected origin to prevent data leakage if the iframe navigates to a different site.</violation>

<violation number="2" location="packages/mesh-plugin-site-editor/client/lib/use-editor-messages.ts:37">
P2: Validate `e.origin` before handling messages so a navigated or compromised iframe can’t send trusted messages from an unexpected origin.</violation>
</file>

<file name="packages/mesh-plugin-site-editor/client/components/branch-switcher.tsx">

<violation number="1" location="packages/mesh-plugin-site-editor/client/components/branch-switcher.tsx:49">
P2: Draft detection is hardcoded to `main`, which can mislabel the published/default branch when it isn’t named `main`. Use the draft prefix (or default branch from data) to determine draft status.</violation>
</file>

<file name="packages/mesh-plugin-site-editor/client/components/page-composer.tsx">

<violation number="1" location="packages/mesh-plugin-site-editor/client/components/page-composer.tsx:119">
P1: Side effect during render: `queueMicrotask(() => send(...))` is called in the render body. React render functions should be pure — in Strict Mode, React double-invokes render, so this will send duplicate `deco:page-config` messages to the iframe. Move this to a `useEffect` keyed on `blocks`.</violation>

<violation number="2" location="packages/mesh-plugin-site-editor/client/components/page-composer.tsx:132">
P2: `useSyncExternalStore` is misused here as an event listener registration mechanism. The snapshot always returns `null`, so `notify()` never causes re-renders — the actual work is done via refs as side effects. Use a `useEffect` instead, which is the standard pattern for DOM event listeners and is much clearer to readers.</violation>
</file>

<file name="apps/mesh/src/web/components/binding-selector.tsx">

<violation number="1" location="apps/mesh/src/web/components/binding-selector.tsx:201">
P3: Folder name extraction only splits on `/`, so Windows paths with backslashes produce incorrect titles. Split on both separators to derive the final folder name reliably.</violation>
</file>

<file name="packages/mesh-plugin-site-editor/server/scanner/schema.ts">

<violation number="1" location="packages/mesh-plugin-site-editor/server/scanner/schema.ts:115">
P2: Inlining $ref discards sibling keywords (description/title/default), so metadata on a $ref node is lost after resolution. Merge the resolved definition with the original node (excluding $ref) to preserve overrides.</violation>
</file>

<file name="packages/mesh-plugin-site-editor/client/lib/use-iframe-bridge.ts">

<violation number="1" location="packages/mesh-plugin-site-editor/client/lib/use-iframe-bridge.ts:56">
P2: `postMessage(msg, "*")` sends editor data (page config, block selections) to any origin. If the iframe navigates to an untrusted page, sensitive data could be leaked. Consider passing the expected site origin instead of `"*"`.</violation>

<violation number="2" location="packages/mesh-plugin-site-editor/client/lib/use-iframe-bridge.ts:121">
P2: `removeEventListener("load", handleIframeLoad)` will silently fail because `handleIframeLoad` is a new function reference each render. The function passed to `addEventListener` in a prior render is a different object than the one passed to `removeEventListener` here. Store the handler in a ref to guarantee the same reference is used for both add and remove.</violation>
</file>

<file name="packages/mesh-plugin-site-editor/server/site-proxy.ts">

<violation number="1" location="packages/mesh-plugin-site-editor/server/site-proxy.ts:78">
P1: Normalize relative paths to prevent `..` segments from escaping the allowed root directory before concatenating them.</violation>

<violation number="2" location="packages/mesh-plugin-site-editor/server/site-proxy.ts:147">
P2: Preserve error responses from the underlying MCP tool before transforming LIST_FILES output so failures aren’t silently treated as empty results.</violation>
</file>

<file name="packages/mesh-plugin-site-editor/client/components/preview-panel.tsx">

<violation number="1" location="packages/mesh-plugin-site-editor/client/components/preview-panel.tsx:75">
P2: Validate the user-provided preview URL before persisting it; otherwise a `javascript:`/`data:` URL can be stored and executed when the iframe loads.</violation>
</file>

<file name="packages/mesh-plugin-site-editor/server/tools/loader-scan.ts">

<violation number="1" location="packages/mesh-plugin-site-editor/server/tools/loader-scan.ts:77">
P2: Handle MCP proxy creation inside the try/catch so connection failures are captured in `errors` instead of crashing the tool.</violation>
</file>

<file name="packages/mesh-plugin-site-editor/server/tools/page-get.ts">

<violation number="1" location="packages/mesh-plugin-site-editor/server/tools/page-get.ts:28">
P1: Validate/sanitize pageId before using it in the file path to prevent path traversal (e.g., only allow expected page ID format).</violation>
</file>

<file name="packages/mesh-plugin-site-editor/client/components/block-detail.tsx">

<violation number="1" location="packages/mesh-plugin-site-editor/client/components/block-detail.tsx:73">
P2: Avoid calling setFormData during render. Render-phase updates can cause extra render passes and warnings in concurrent rendering; sync block defaults in a useEffect instead.</violation>
</file>

<file name="packages/bindings/src/core/plugin-router.tsx">

<violation number="1" location="packages/bindings/src/core/plugin-router.tsx:41">
P2: `prependBasePath` now removes the first path segment for all absolute plugin routes, which drops legitimate segments when the path does not include an ID-only layout prefix (e.g., `"/$appName"` becomes just the base path). Guard the stripping logic so it only removes known layout IDs (e.g., segments ending in `-layout`) and otherwise preserve the original absolute path.</violation>
</file>

<file name="packages/mesh-plugin-site-editor/server/tools/loader-list.ts">

<violation number="1" location="packages/mesh-plugin-site-editor/server/tools/loader-list.ts:87">
P2: Guard against missing `label` values before sorting. As written, an undefined label from a malformed loader JSON will cause `localeCompare` to throw and the tool to fail.</violation>
</file>

<file name="packages/mesh-plugin-site-editor/server/scanner/discover.ts">

<violation number="1" location="packages/mesh-plugin-site-editor/server/scanner/discover.ts:32">
P2: The bare `"Element"` pattern is too broad for a substring match. It will match any return type containing the word "Element" (e.g., `HTMLElement`, `SVGElement`, `CustomElement`), causing false positives in component detection and false negatives in loader detection. The legitimate cases are already covered by `"JSX.Element"` and `"ReactElement"`.</violation>

<violation number="2" location="packages/mesh-plugin-site-editor/server/scanner/discover.ts:289">
P2: Bug: JSDoc is never extracted for arrow function components. The parent traversal stops one level too short — `fn.getParent().getParent()` reaches `VariableDeclarationList`, not `VariableStatement` (which is where JSDoc is attached in the TS AST). You need to go up one more level.</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.

description: "Delete a CMS page by ID.",
inputSchema: z.object({
connectionId: z.string().describe("MCP connection ID for the site"),
pageId: z.string().describe("Page ID to delete"),
Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot Feb 15, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1: Validate/sanitize pageId before building the file path; as written, arbitrary path segments (e.g., "../") can escape .deco/pages and delete/overwrite unintended files.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/mesh-plugin-site-editor/server/tools/page-delete.ts, line 22:

<comment>Validate/sanitize pageId before building the file path; as written, arbitrary path segments (e.g., "../") can escape `.deco/pages` and delete/overwrite unintended files.</comment>

<file context>
@@ -0,0 +1,70 @@
+  description: "Delete a CMS page by ID.",
+  inputSchema: z.object({
+    connectionId: z.string().describe("MCP connection ID for the site"),
+    pageId: z.string().describe("Page ID to delete"),
+  }),
+  outputSchema: z.object({
</file context>
Fix with Cubic

"Get a single CMS loader definition by ID, including its full input and output JSON Schemas.",
inputSchema: z.object({
connectionId: z.string().describe("MCP connection ID for the site"),
loaderId: z.string().describe('Loader ID (e.g., "loaders--productList")'),
Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot Feb 15, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1: Validate loaderId to prevent path traversal before using it in a file path.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/mesh-plugin-site-editor/server/tools/loader-get.ts, line 18:

<comment>Validate `loaderId` to prevent path traversal before using it in a file path.</comment>

<file context>
@@ -0,0 +1,77 @@
+    "Get a single CMS loader definition by ID, including its full input and output JSON Schemas.",
+  inputSchema: z.object({
+    connectionId: z.string().describe("MCP connection ID for the site"),
+    loaderId: z.string().describe('Loader ID (e.g., "loaders--productList")'),
+  }),
+  outputSchema: z.object({
</file context>
Fix with Cubic

*/
function toAbsolute(rootDir: string | null, relativePath: string): string {
if (!rootDir || relativePath.startsWith("/")) return relativePath;
return `${rootDir.replace(/\/$/, "")}/${relativePath}`;
Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot Feb 15, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1: Normalize relative paths to prevent .. segments from escaping the allowed root directory before concatenating them.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/mesh-plugin-site-editor/server/site-proxy.ts, line 78:

<comment>Normalize relative paths to prevent `..` segments from escaping the allowed root directory before concatenating them.</comment>

<file context>
@@ -0,0 +1,184 @@
+ */
+function toAbsolute(rootDir: string | null, relativePath: string): string {
+  if (!rootDir || relativePath.startsWith("/")) return relativePath;
+  return `${rootDir.replace(/\/$/, "")}/${relativePath}`;
+}
+
</file context>
Fix with Cubic

Comment on lines +41 to +44
const rest = to.substring(1); // "site-editor-layout/pages/$pageId"
const slashIdx = rest.indexOf("/");
const urlPath = slashIdx >= 0 ? rest.substring(slashIdx) : "";
return `/${org}/${project}/${pluginId}${urlPath}`;
Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot Feb 15, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: prependBasePath now removes the first path segment for all absolute plugin routes, which drops legitimate segments when the path does not include an ID-only layout prefix (e.g., "/$appName" becomes just the base path). Guard the stripping logic so it only removes known layout IDs (e.g., segments ending in -layout) and otherwise preserve the original absolute path.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/bindings/src/core/plugin-router.tsx, line 41:

<comment>`prependBasePath` now removes the first path segment for all absolute plugin routes, which drops legitimate segments when the path does not include an ID-only layout prefix (e.g., `"/$appName"` becomes just the base path). Guard the stripping logic so it only removes known layout IDs (e.g., segments ending in `-layout`) and otherwise preserve the original absolute path.</comment>

<file context>
@@ -28,15 +33,36 @@ function prependBasePath(
+    // e.g., "/site-editor-layout/pages/$pageId" → "/pages/$pageId"
+    // e.g., "/site-editor-layout/" → "/"
+    // e.g., "/site-editor-layout" → ""
+    const rest = to.substring(1); // "site-editor-layout/pages/$pageId"
+    const slashIdx = rest.indexOf("/");
+    const urlPath = slashIdx >= 0 ? rest.substring(slashIdx) : "";
</file context>
Suggested change
const rest = to.substring(1); // "site-editor-layout/pages/$pageId"
const slashIdx = rest.indexOf("/");
const urlPath = slashIdx >= 0 ? rest.substring(slashIdx) : "";
return `/${org}/${project}/${pluginId}${urlPath}`;
const rest = to.substring(1); // "site-editor-layout/pages/$pageId"
const slashIdx = rest.indexOf("/");
const firstSegment = slashIdx >= 0 ? rest.substring(0, slashIdx) : rest;
const urlPath = firstSegment.endsWith("-layout")
? slashIdx >= 0
? rest.substring(slashIdx)
: ""
: `/${rest}`;
return `/${org}/${project}/${pluginId}${urlPath}`;
Fix with Cubic

loaders.push({
id: loader.id,
source: loader.source,
label: loader.label,
Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot Feb 15, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: Guard against missing label values before sorting. As written, an undefined label from a malformed loader JSON will cause localeCompare to throw and the tool to fail.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/mesh-plugin-site-editor/server/tools/loader-list.ts, line 87:

<comment>Guard against missing `label` values before sorting. As written, an undefined label from a malformed loader JSON will cause `localeCompare` to throw and the tool to fail.</comment>

<file context>
@@ -0,0 +1,105 @@
+          loaders.push({
+            id: loader.id,
+            source: loader.source,
+            label: loader.label,
+            category: loader.category ?? "Other",
+            inputParamsCount,
</file context>
Fix with Cubic

"ReactElement",
"React.FC",
"React.FunctionComponent",
"Element",
Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot Feb 15, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: The bare "Element" pattern is too broad for a substring match. It will match any return type containing the word "Element" (e.g., HTMLElement, SVGElement, CustomElement), causing false positives in component detection and false negatives in loader detection. The legitimate cases are already covered by "JSX.Element" and "ReactElement".

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/mesh-plugin-site-editor/server/scanner/discover.ts, line 32:

<comment>The bare `"Element"` pattern is too broad for a substring match. It will match any return type containing the word "Element" (e.g., `HTMLElement`, `SVGElement`, `CustomElement`), causing false positives in component detection and false negatives in loader detection. The legitimate cases are already covered by `"JSX.Element"` and `"ReactElement"`.</comment>

<file context>
@@ -0,0 +1,546 @@
+  "ReactElement",
+  "React.FC",
+  "React.FunctionComponent",
+  "Element",
+];
+
</file context>
Fix with Cubic

if (!parent) return "";

// Walk up to the variable statement which can hold JSDoc
const varStatement = parent.getParent();
Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot Feb 15, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: Bug: JSDoc is never extracted for arrow function components. The parent traversal stops one level too short — fn.getParent().getParent() reaches VariableDeclarationList, not VariableStatement (which is where JSDoc is attached in the TS AST). You need to go up one more level.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/mesh-plugin-site-editor/server/scanner/discover.ts, line 289:

<comment>Bug: JSDoc is never extracted for arrow function components. The parent traversal stops one level too short — `fn.getParent().getParent()` reaches `VariableDeclarationList`, not `VariableStatement` (which is where JSDoc is attached in the TS AST). You need to go up one more level.</comment>

<file context>
@@ -0,0 +1,546 @@
+  if (!parent) return "";
+
+  // Walk up to the variable statement which can hold JSDoc
+  const varStatement = parent.getParent();
+  if (varStatement?.isKind(SyntaxKind.VariableStatement)) {
+    const jsDocs = varStatement.getJsDocs();
</file context>
Fix with Cubic

if (!folderPath) return;

const folderName =
folderPath.split("/").filter(Boolean).pop() ?? "folder";
Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot Feb 15, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3: Folder name extraction only splits on /, so Windows paths with backslashes produce incorrect titles. Split on both separators to derive the final folder name reliably.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/mesh/src/web/components/binding-selector.tsx, line 201:

<comment>Folder name extraction only splits on `/`, so Windows paths with backslashes produce incorrect titles. Split on both separators to derive the final folder name reliably.</comment>

<file context>
@@ -149,8 +165,82 @@ export function BindingSelector({
+      if (!folderPath) return;
+
+      const folderName =
+        folderPath.split("/").filter(Boolean).pop() ?? "folder";
+
+      // For object storage bindings, use the local-object-storage bridge
</file context>
Suggested change
folderPath.split("/").filter(Boolean).pop() ?? "folder";
folderPath.split(/[/\\]/).filter(Boolean).pop() ?? "folder";
Fix with Cubic

vibegui and others added 27 commits February 18, 2026 13:16
…out helpers

- getGitStatus() calls GIT_STATUS and returns file status or null on error
- getCommittedPage() calls GIT_SHOW HEAD and parses blocks array or null on error
- discardPageChanges() calls GIT_CHECKOUT with force:true, returns boolean success
- pendingChanges.page(connectionId, pageId) returns stable cache key
- sits alongside existing history, pages, blocks, loaders keys
- useQuery-based (no useEffect) fetching GIT_STATUS + GIT_SHOW on demand
- computeSectionStatuses pure function classifies new/edited/deleted sections
- enabled: isDirty guard prevents GIT_SHOW call when file is clean
- returns { sectionStatuses, isDirty, isLoading }
- SortableSectionItem accepts optional status prop (new/edited) for colored badges
- DeletedSectionGhostRow renders deleted sections as greyed-out italic rows
- SectionListSidebar accepts sectionStatuses and onUndelete optional props
- Ghost rows render below live list in dashed-border section with Undelete button
- Empty state check handles case where blocks is empty but deleted ghosts exist
- Import usePendingChanges hook and discardPageChanges API helper
- Add XCircle to @untitledui/icons import for Discard button icon
- Call usePendingChanges(toolCaller, connectionId, pageId, blocks) for diff status
- Invalidate pendingChanges query key after debouncedSave and handleSave
- handleDiscard: cancels debounce timer, calls discardPageChanges, refetches page
- handleUndelete: appends committed block back to blocks and triggers debouncedSave
- Discard Changes button in toolbar, visible only when gitIsDirty is true
- Pass sectionStatuses and onUndelete props to SectionListSidebar
- Create 12-01-SUMMARY.md with full execution record
- Update STATE.md: Phase 12 complete, decisions added, session info updated
- Update ROADMAP.md: Phase 12 checked off as complete
- Mark requirements DIFF-01 through DIFF-05 as complete in REQUIREMENTS.md
…CommitMessage)

- getDiff() wraps GIT_DIFF tool call, returns null on failure
- gitCommit() wraps GIT_COMMIT tool call, returns null on failure
- generateCommitMessage() POSTs diff to server-side Haiku route
- Add GitLogEntry interface (hash, author, date, message)
- Add getGitLog() using GIT_LOG tool with graceful null-on-error
- Add getGitShow() using GIT_SHOW tool with graceful null-on-error
- Preserve existing getFileHistory/readFileAt/revertPage functions
- POST /api/plugins/site-editor/commit-message accepts { diff }
- Calls Anthropic Messages API via fetch (no new dependencies)
- Falls back to empty message when ANTHROPIC_API_KEY not set or API errors
- Add routes property to serverPlugin
- Mount POST /commit-message via registerCommitMessageRoute
- Replace GET_FILE_HISTORY/READ_FILE_AT with GIT_LOG/GIT_SHOW
- Add send and localPage props for iframe bridge communication
- Show commit list with short hash, relative date, truncated message
- handlePreviewCommit fetches GIT_SHOW and sends deco:page-config to iframe
- Amber banner with 'Back to current' button when previewing historical version
- Per-row loading spinner, disable preview buttons during fetch
- Loading/error/empty states with appropriate messaging
…mposer

- PageHistory now receives send (iframe bridge) and localPage (live page)
- Enables PageHistory to push deco:page-config to iframe and restore on back
Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
- Add CommitState type (idle/generating/editing/committing)
- handleCommitClick: fetch diff then generate AI message
- handleCommitConfirm: call gitCommit then invalidate pendingChanges
- handleCommitCancel: reset to idle
- Commit + Discard buttons shown only when gitIsDirty and mode is idle
- CommitDialog shown inline when commit flow is in progress
- Fix commit-message.ts: extract Hono type from ServerPlugin to avoid direct hono dep
- New revertToCommit(toolCaller, pageId, commitHash) function
- Uses GIT_SHOW to read historical content, PUT_FILE to write, GIT_COMMIT for git record
- Returns { success, committedWithGit } — gracefully degrades if GIT_COMMIT not supported
- Preserves existing revertPage function unchanged
- Add onRevert prop to PageHistoryProps interface
- Add confirmingRevertHash and revertingHash state
- Add handleRevert async function calling revertToCommit
- Per-commit row: Revert here button -> Are you sure? confirmation
- Spinner on active revert, Cancel button hidden when reverting
- handlePreviewCommit clears confirmation state on each preview click
- Import revertToCommit and RefreshCcw01 icon
- Add handleRevert: closes history panel, invalidates pages.detail and history.page queries
- Pass onRevert={handleRevert} to <PageHistory>
- Section list sidebar auto-refreshes via page query invalidation after revert
Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

4 issues found across 174 files (changes from recent commits).

Note: This PR contains a large number of files. cubic only reviews up to 75 files per PR, so some files may not have been reviewed.

Prompt for AI agents (all issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name=".claude/agents/gsd-verifier.md">

<violation number="1" location=".claude/agents/gsd-verifier.md:58">
P2: The grep pattern `^| $PHASE_NUM` matches every line (the `^` alternative always succeeds), so the phase filter never works. Use a grouped regex so it only matches lines containing the phase number.</violation>
</file>

<file name=".claude/commands/gsd/complete-milestone.md">

<violation number="1" location=".claude/commands/gsd/complete-milestone.md:93">
P2: Step 5 hard-codes `v1` requirements, which will mislead users when completing other milestone versions. It should reference the current `{{version}}` instead of v1.</violation>
</file>

<file name=".claude/agents/gsd-phase-researcher.md">

<violation number="1" location=".claude/agents/gsd-phase-researcher.md:364">
P2: The guidance contradicts itself about the required first section format in RESEARCH.md (plain `## User Constraints` vs. `<user_constraints>` wrapper). This ambiguity can break the planner’s parsing expectations. Align both sections to a single required format.</violation>

<violation number="2" location=".claude/agents/gsd-phase-researcher.md:473">
P3: The success criteria contradicts the earlier "commit optional" instruction, which can cause confusion about whether committing is required. Tie the criterion to `commit_docs` or make the step mandatory consistently.</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.

ls "$PHASE_DIR"/*-PLAN.md 2>/dev/null
ls "$PHASE_DIR"/*-SUMMARY.md 2>/dev/null
node ./.claude/get-shit-done/bin/gsd-tools.cjs roadmap get-phase "$PHASE_NUM"
grep -E "^| $PHASE_NUM" .planning/REQUIREMENTS.md 2>/dev/null
Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot Feb 19, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: The grep pattern ^| $PHASE_NUM matches every line (the ^ alternative always succeeds), so the phase filter never works. Use a grouped regex so it only matches lines containing the phase number.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At .claude/agents/gsd-verifier.md, line 58:

<comment>The grep pattern `^| $PHASE_NUM` matches every line (the `^` alternative always succeeds), so the phase filter never works. Use a grouped regex so it only matches lines containing the phase number.</comment>

<file context>
@@ -0,0 +1,555 @@
+ls "$PHASE_DIR"/*-PLAN.md 2>/dev/null
+ls "$PHASE_DIR"/*-SUMMARY.md 2>/dev/null
+node ./.claude/get-shit-done/bin/gsd-tools.cjs roadmap get-phase "$PHASE_NUM"
+grep -E "^| $PHASE_NUM" .planning/REQUIREMENTS.md 2>/dev/null
+```
+
</file context>
Fix with Cubic

5. **Archive requirements:**

- Create `.planning/milestones/v{{version}}-REQUIREMENTS.md`
- Mark all v1 requirements as complete (checkboxes checked)
Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot Feb 19, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: Step 5 hard-codes v1 requirements, which will mislead users when completing other milestone versions. It should reference the current {{version}} instead of v1.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At .claude/commands/gsd/complete-milestone.md, line 93:

<comment>Step 5 hard-codes `v1` requirements, which will mislead users when completing other milestone versions. It should reference the current `{{version}}` instead of v1.</comment>

<file context>
@@ -0,0 +1,136 @@
+5. **Archive requirements:**
+
+   - Create `.planning/milestones/v{{version}}-REQUIREMENTS.md`
+   - Mark all v1 requirements as complete (checkboxes checked)
+   - Note requirement outcomes (validated, adjusted, dropped)
+   - Delete `.planning/REQUIREMENTS.md` (fresh one created for next milestone)
</file context>
Fix with Cubic


**ALWAYS use Write tool to persist to disk** — mandatory regardless of `commit_docs` setting.

**CRITICAL: If CONTEXT.md exists, FIRST content section MUST be `<user_constraints>`:**
Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot Feb 19, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: The guidance contradicts itself about the required first section format in RESEARCH.md (plain ## User Constraints vs. <user_constraints> wrapper). This ambiguity can break the planner’s parsing expectations. Align both sections to a single required format.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At .claude/agents/gsd-phase-researcher.md, line 364:

<comment>The guidance contradicts itself about the required first section format in RESEARCH.md (plain `## User Constraints` vs. `<user_constraints>` wrapper). This ambiguity can break the planner’s parsing expectations. Align both sections to a single required format.</comment>

<file context>
@@ -0,0 +1,484 @@
+
+**ALWAYS use Write tool to persist to disk** — mandatory regardless of `commit_docs` setting.
+
+**CRITICAL: If CONTEXT.md exists, FIRST content section MUST be `<user_constraints>`:**
+
+```markdown
</file context>
Fix with Cubic

- [ ] Source hierarchy followed (Context7 → Official → WebSearch)
- [ ] All findings have confidence levels
- [ ] RESEARCH.md created in correct format
- [ ] RESEARCH.md committed to git
Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot Feb 19, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3: The success criteria contradicts the earlier "commit optional" instruction, which can cause confusion about whether committing is required. Tie the criterion to commit_docs or make the step mandatory consistently.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At .claude/agents/gsd-phase-researcher.md, line 473:

<comment>The success criteria contradicts the earlier "commit optional" instruction, which can cause confusion about whether committing is required. Tie the criterion to `commit_docs` or make the step mandatory consistently.</comment>

<file context>
@@ -0,0 +1,484 @@
+- [ ] Source hierarchy followed (Context7 → Official → WebSearch)
+- [ ] All findings have confidence levels
+- [ ] RESEARCH.md created in correct format
+- [ ] RESEARCH.md committed to git
+- [ ] Structured return provided to orchestrator
+
</file context>
Fix with Cubic

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant